﻿// Copyright (c) 2014 Converter Systems LLC

using ConverterSystems.Collections;
using Opc.Ua;
using Opc.Ua.Client;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;

namespace ConverterSystems.ServiceModel
{
    public static class ServiceExtensions
    {
        /// <summary>
        /// Ensures the application certificate is present and valid.
        /// </summary>
        public static void EnsureApplicationCertificate(this ApplicationConfiguration configuration)
        {
            const ushort keySize = 1024;
            const ushort lifetimeInMonths = 300;

            if (configuration == null)
            {
                throw new ArgumentNullException("configuration");
            }
            bool errorFlag = false;
            string hostName = Dns.GetHostName();
            var serverDomainNames = configuration.GetServerDomainNames();
            var applicationCertificate = configuration.SecurityConfiguration.ApplicationCertificate;
            var certificate = applicationCertificate.Find(true);
            if (certificate != null)
            {
                // if cert found then check domains
                var domainsFromCertficate = Utils.GetDomainsFromCertficate(certificate);
                foreach (string serverDomainName in serverDomainNames)
                {
                    if (Utils.FindStringIgnoreCase(domainsFromCertficate, serverDomainName))
                    {
                        continue;
                    }
                    if (String.Equals(serverDomainName, "localhost", StringComparison.OrdinalIgnoreCase))
                    {
                        if (Utils.FindStringIgnoreCase(domainsFromCertficate, hostName))
                        {
                            continue;
                        }
                        var hostEntry = Dns.GetHostEntry(hostName);
                        if (hostEntry.Aliases.Any(alias => Utils.FindStringIgnoreCase(domainsFromCertficate, alias)))
                        {
                            continue;
                        }
                        if (hostEntry.AddressList.Any(ipAddress => Utils.FindStringIgnoreCase(domainsFromCertficate, ipAddress.ToString())))
                        {
                            continue;
                        }
                    }
                    Trace.TraceInformation("The application is configured to use domain '{0}' which does not appear in the certificate.", serverDomainName);
                    errorFlag = true;
                } // end for
                // if no errors and keySizes match
                if (!errorFlag && (keySize == certificate.PublicKey.Key.KeySize))
                {
                    configuration.CertificateValidator.CertificateValidation += (s, e) => Trace.TraceInformation(e.Error.ToString());
                    return; // cert okay
                }
            }
            // if we get here then we'll create a new cert
            if (certificate == null)
            {
                certificate = applicationCertificate.Find(false);
                if (certificate != null)
                {
                    Trace.TraceInformation("Matching certificate with SubjectName '{0}' found but without a private key.", applicationCertificate.SubjectName);
                }
            }
            // lets check if there is any to delete
            if (certificate != null)
            {
                using (var store2 = applicationCertificate.OpenStore())
                {
                    store2.Delete(certificate.Thumbprint);
                }
            }
            if (serverDomainNames.Count == 0)
            {
                serverDomainNames.Add(hostName);
            }
            CertificateFactory.CreateCertificate(applicationCertificate.StoreType, applicationCertificate.StorePath, configuration.ApplicationUri, configuration.ApplicationName, null, serverDomainNames, keySize, lifetimeInMonths);
            Trace.TraceInformation("Created new certificate with SubjectName '{0}', in certificate store '{1}'.", applicationCertificate.SubjectName, applicationCertificate.StorePath);
            configuration.CertificateValidator.Update(configuration.SecurityConfiguration);
            configuration.CertificateValidator.CertificateValidation += (s, e) => Trace.TraceInformation(e.Error.ToString());
        }

        /// <summary>
        /// Writes the value to the node of the server.
        /// </summary>
        public static async Task WriteValueAsync(this UaSession session, NodeId nodeId, DataValue value, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (session == null) throw new ArgumentNullException("session");
            if (value == null) throw new ArgumentNullException("value");
            if (nodeId == null) throw new ArgumentNullException("nodeId");
            if (!session.Connected) throw new ServiceResultException(StatusCodes.BadSessionClosed);
            var writeRequest = new WriteRequest { NodesToWrite = { new WriteValue { NodeId = nodeId, AttributeId = Attributes.Value, Value = value } } };
            var writeResponse = await session.WriteAsync(writeRequest, cancellationToken).ConfigureAwait(false);
            if (StatusCode.IsNotGood(writeResponse.Results[0]))
            {
                throw new ServiceResultException(new ServiceResult(writeResponse.Results[0]));
            }
        }

        /// <summary>
        /// Writes the value to the node of the server.
        /// </summary>
        public static async Task WriteValueAsync(this UaSession session, NodeId nodeId, Object value, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (session == null) throw new ArgumentNullException("session");
            if (value == null) throw new ArgumentNullException("value");
            if (nodeId == null) throw new ArgumentNullException("nodeId");
            if (!session.Connected) throw new ServiceResultException(StatusCodes.BadSessionClosed);
            var writeRequest = new WriteRequest { NodesToWrite = { new WriteValue { NodeId = nodeId, AttributeId = Attributes.Value, Value = new DataValue(new Variant(value)) } } };
            var writeResponse = await session.WriteAsync(writeRequest, cancellationToken).ConfigureAwait(false);
            if (StatusCode.IsNotGood(writeResponse.Results[0]))
            {
                throw new ServiceResultException(new ServiceResult(writeResponse.Results[0]));
            }
        }

        /// <summary>
        /// Calls a method of the server.
        /// </summary>
        /// <returns>
        /// A task that returns the output arguments, as an object[].
        /// </returns>
        public static async Task<Object[]> CallAsync(this UaSession session, NodeId objectId, NodeId methodId, Object[] inArgs, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (session == null) throw new ArgumentNullException("session");
            if (objectId == null) throw new ArgumentNullException("objectId");
            if (methodId == null) throw new ArgumentNullException("methodId");
            if (inArgs == null) throw new ArgumentNullException("inArgs");
            if (!session.Connected) throw new ServiceResultException(StatusCodes.BadSessionClosed);
            var callRequest = new CallRequest { MethodsToCall = { new CallMethodRequest { ObjectId = objectId, MethodId = methodId, InputArguments = inArgs.Select(a => new Variant(a)).ToArray() } } };
            var callResponse = await session.CallAsync(callRequest, cancellationToken).ConfigureAwait(false);
            if (StatusCode.IsNotGood(callResponse.Results[0].StatusCode))
            {
                throw new ServiceResultException(new ServiceResult(callResponse.Results[0].StatusCode));
            }
            return callResponse.Results[0].OutputArguments.Select(a => a.Value).ToArray();
        }

        /// <summary>
        /// Prepares to configure this subscription with bindings.
        /// </summary>
        /// <remarks>
        /// Follow this method with:
        /// .AddBinding - To add a binding to a property of the model.
        /// .AddBindings - To add bindings to all the properties specified with a [MonitoredItem] attribute.
        /// </remarks>
        public static ModelSubscription<TModel> WithModel<TModel>(this Subscription subscription, TModel model)
        {
            return new ModelSubscription<TModel>(subscription, model);
        }

        /// <summary>
        /// Configures this subscription with bindings to the properties of a model.
        /// </summary>
        /// <remarks>
        /// The properties specified with a [MonitoredItem] attribute are added to the subscription.
        /// </remarks>
        public static ModelSubscription<TModel> AddBindings<TModel>(this ModelSubscription<TModel> tuple, string startNodeId = null)
        {
            var model = tuple.Model;
            var subscription = tuple.Subscription;
            var typeInfo = model.GetType().GetTypeInfo();
            foreach (var propertyInfo in typeInfo.DeclaredProperties)
            {
                var itemAttribute = propertyInfo.GetCustomAttribute<MonitoredItemAttribute>();
                if (itemAttribute == null || !propertyInfo.CanRead || !propertyInfo.CanWrite)
                {
                    continue;
                }
                var item = new MonitoredItem(subscription.DefaultItem) { DisplayName = propertyInfo.Name, StartNodeId = startNodeId ?? itemAttribute.StartNodeId, RelativePath = itemAttribute.RelativePath, IndexRange = itemAttribute.IndexRange, AttributeId = itemAttribute.AttributeId, SamplingInterval = itemAttribute.SamplingInterval, QueueSize = itemAttribute.QueueSize, DiscardOldest = itemAttribute.DiscardOldest, CacheQueueSize = itemAttribute.CacheQueueSize };
                if (itemAttribute.AttributeId == Attributes.EventNotifier)
                {
                    item.NodeClass = NodeClass.Object;
                    if (!string.IsNullOrEmpty(itemAttribute.Filter))
                    {
                        var filter = new EventFilter();
                        using (var stringReader = new StringReader(itemAttribute.Filter))
                        using (var xmlReader = XmlReader.Create(stringReader))
                        using (var decoder = new XmlDecoder(typeof(EventFilter), xmlReader, ServiceMessageContext.GlobalContext))
                        {
                            filter.Decode(decoder);
                        }
                        item.Filter = filter;
                    }
                }
                if (itemAttribute.AttributeId == Attributes.Value)
                {
                    item.NodeClass = NodeClass.Variable;
                    if (!string.IsNullOrEmpty(itemAttribute.Filter))
                    {
                        var filter = new DataChangeFilter();
                        using (var stringReader = new StringReader(itemAttribute.Filter))
                        using (var xmlReader = XmlReader.Create(stringReader))
                        using (var decoder = new XmlDecoder(typeof(DataChangeFilter), xmlReader, ServiceMessageContext.GlobalContext))
                        {
                            filter.Decode(decoder);
                        }
                        item.Filter = filter;
                    }
                }
                var propType = propertyInfo.PropertyType;
                if (propType == typeof(DataValue))
                {
                    item.Handle = new DataValueBinding(subscription, item, model, propertyInfo);
                    subscription.AddItem(item);
                    continue;
                }
                if (propType == typeof(ObservableQueue<DataValue>))
                {
                    item.Handle = new DataValueQueueBinding(subscription, item, model, propertyInfo);
                    subscription.AddItem(item);
                    continue;
                }
                if (propType == typeof(EventFieldList))
                {
                    item.Handle = new EventBinding(subscription, item, model, propertyInfo);
                    subscription.AddItem(item);
                    continue;
                }
                if (propType == typeof(ObservableQueue<EventFieldList>))
                {
                    item.Handle = new EventQueueBinding(subscription, item, model, propertyInfo);
                    subscription.AddItem(item);
                    continue;
                }
                if (propType == typeof(Func<Object[], Task<Object[]>>))
                {
                    item.MonitoringMode = MonitoringMode.Disabled;
                    subscription.AddItem(item);
                    Func<Object[], Task<Object[]>> f = (inArgs) => ((UaSession)item.Subscription.Session).CallAsync(item.StartNodeId, item.ResolvedNodeId, inArgs);
                    propertyInfo.SetValue(model, f);
                    continue;
                }
                item.Handle = new ValueBinding(subscription, item, model, propertyInfo);
                subscription.AddItem(item);
            }
            return tuple;
        }

        /// <summary>
        /// Configures this subscription with a binding to a property.
        /// </summary>
        /// <remarks>
        /// The MonitoredItem specified will be bound to the property and added to the subscription.
        /// </remarks>

        public static ModelSubscription<TModel> AddBinding<TModel, TResult>(this ModelSubscription<TModel> tuple, Expression<Func<TModel, TResult>> propertyExpression, MonitoredItem item)
        {
            if (propertyExpression == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }
            var memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("propertyExpression");
            }
            var propertyInfo = memberExpression.Member as PropertyInfo;
            if (propertyInfo == null)
            {
                throw new ArgumentException("propertyExpression");
            }
            if (item == null) throw new ArgumentNullException("item");

            if (!propertyInfo.CanRead || !propertyInfo.CanWrite)
            {
                return tuple;
            }
            var model = tuple.Model;
            var subscription = tuple.Subscription;
            item.DisplayName = propertyInfo.Name;
            item.MonitoringMode = MonitoringMode.Reporting;
            var propType = propertyInfo.PropertyType;
            if (propType == typeof(DataValue))
            {
                item.Handle = new DataValueBinding(subscription, item, model, propertyInfo);
                subscription.AddItem(item);
                return tuple;
            }
            if (propType == typeof(ObservableQueue<DataValue>))
            {
                item.Handle = new DataValueQueueBinding(subscription, item, model, propertyInfo);
                subscription.AddItem(item);
                return tuple;
            }
            if (propType == typeof(EventFieldList))
            {
                item.Handle = new EventBinding(subscription, item, model, propertyInfo);
                subscription.AddItem(item);
                return tuple;
            }
            if (propType == typeof(ObservableQueue<EventFieldList>))
            {
                item.Handle = new EventQueueBinding(subscription, item, model, propertyInfo);
                subscription.AddItem(item);
                return tuple;
            }
            if (propType == typeof(Func<Object[], Task<Object[]>>))
            {
                item.MonitoringMode = MonitoringMode.Disabled;
                subscription.AddItem(item);
                Func<Object[], Task<Object[]>> f = (inArgs) => ((UaSession)item.Subscription.Session).CallAsync(item.StartNodeId, item.ResolvedNodeId, inArgs);
                propertyInfo.SetValue(model, f);
                return tuple;
            }
            item.Handle = new ValueBinding(subscription, item, model, propertyInfo);
            subscription.AddItem(item);
            return tuple;
        }
    }

    /// <summary>
    /// A Model and Subscription pair.
    /// </summary>
    public class ModelSubscription<TModel>
    {
        public ModelSubscription(Subscription subscription, TModel model)
        {
            Subscription = subscription;
            Model = model;
        }

        public Subscription Subscription { get; private set; }

        public TModel Model { get; private set; }

        public static implicit operator Subscription(ModelSubscription<TModel> tuple)
        {
            return tuple.Subscription;
        }
    }
}